# Flow-IPC: Sessions
# Copyright 2023 Akamai Technologies, Inc.
#
# Licensed under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in
# compliance with the License.  You may obtain a copy
# of the License at
#
#   https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in
# writing, software distributed under the License is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing
# permissions and limitations under the License.

@0xf39426463575be4f; # Manually generated by `capnp id` and saved here for all time.

using Cxx = import "/capnp/c++.capnp";

# The following section is, informally, this schema file's *header*.
# Recommendation: copy-paste relevant pieces into other schema files + follow similar conventions in this
# and other library+API combos.
#
# Header BEGIN ---

# Quick namespace reference:
# - ipc::session: Flow-IPC's ipc::session sub-module.
#   - schema: Indicates the symbols therein are auto-generated by capnp from a schema such as in this file.
#     - detail: Not to be used by external user of this library+API but merely internally.
$Cxx.namespace("ipc::session::schema::detail");

# --- session_master_channel.capnp ---
# This schema centers around the ipc::transport::Channel internally used by ipc::session to enable other channels
# and other basic session activity: the "session master channel."
#
# Background: A *session* is the totality of IPC work between a given application A' instance (process) A and
# application B' instance (process) B.  That is, A (called client) connects to B (server), usually around startup, when
# it wants to begin IPC with B'; this establishes the session.  (It ends when either A or B dies or otherwise
# becomes inactive/unresponsive.)  While the session is active, each of A may establish 0+ *channels* with B;
# and symmetrically vice versa.  Each channel, once established, is a full-duplex communication pipe for exchanging
# structured messages -- *structured* as enabled by capnp schemas like this one -- and some other stuff (at least
# native sockets a/k/a FDs).  These channels deal in payloads defined by schemas written by A', B' application coders
# (Flow-IPC library users).  However in order for these channels to be established successfully in the first place,
# a *session master channel* is required to be established by internal ipc::session code.  Its tasks at least
# comprise:
#   - Message exachange for the initial "login" of A to B: while the low-level channel connection establishes the
#     raw channel, certain security and safety-related matters have to be initially exchanged.  Once completed:
#     - If A wants to open a channel to B, or B to A, a channel-opening message exchange must occur.  So perhaps
#       the session master channel is the chicken... while the user channels are the subsequently resulting eggs.
#       That's assuming one believes the chicken comes before the eggs...!
#
# So to summarize: The schema here defines the internally used messages exchanged along each session master channel.

# --- END Header.

# Types used by schema below.

using Common = import "/ipc/transport/struc/schema/common.capnp";
using SessionCommon = import "/ipc/session/schema/common.capnp";
using Metadata = Common.Metadata;
using Size = Common.Size;
using MqType = SessionCommon.MqType;
using ShmType = SessionCommon.ShmType;
using ProcessCredentials = SessionCommon.ProcessCredentials;
using ProtoVer = Int16; # Isomorphic to `Protocol_version::proto_ver_t`.  See discussion in its doc header.

using ClientNamespace = Text;
# String identifying, uniquely within a given half-session (i.e., IPC-active server-app instance (process)),
# a particular session (i.e., connecting client-app instance (process)).
#   - Used, at least, within `Shared_name`s to segregate all `Shared_name`s pertaining to a given session.
#     So, conceptually, each in-session shared resources will have a name like
#     "/.../<half-session-namespace>/.../<ClientNamespace>".
#   - As of this writing it is actually a decimal encoding of an integer 1, 2, ..., assigned in order that
#     new sessions arise within a given half-session.  (In fact see LogInRsp.)

using SharedName = Text;
# Corresponds to ipc::util::Shared_name.

# A distinct client application (as opposed to instance of that application a/k/a process)
struct ClientApp
{
  name @0 :Text;
  # Brief string uniquely, centrally identifying the app to a human or machine.  Manually assigned.
}

enum OpenChannelResult
{
  # OpenChannel*Rsp result (what the other guy says in response to first guy saying they want to open a channel).

  accepted @0;
  # All good.  Channel opened.

  rejectedPassiveOpen @1;
  # User indicated they'd only make open requests -- not accept them.

  rejectedResourceUnavailable @2;
  # (Only possible when server is the passive-opener) Failed to create an underlying transport for some system
  # reason such as, one supposes, exhausted RAM resources or... who knows?

  endSentinel @3;
}

# Main schema.

struct SessionMasterChannelMessageBody(MetadataPayload)
{
  union
  {
    logInReq @0 :LogInReq(MetadataPayload);
    logInRsp @1 :LogInRsp(MetadataPayload);
    jemallocShmSetup @2 :JemallocShmSetup;
    openChannelToClientReq @3 :OpenChannelToClientReq(MetadataPayload);
    openChannelToClientRsp @4 :OpenChannelToClientRsp;
    openChannelToServerReq @5 :OpenChannelToServerReq(MetadataPayload);
    openChannelToServerRsp @6 :OpenChannelToServerRsp;
    gracefulSessionEnd @7 :GracefulSessionEnd;
  } # union
} # struct SessionMasterChannelMessageBody

struct LogInReq(MetadataPayload)
{
  # Client->server log-in requests.  No other messages are allowed until this is received by server and replied-to.
  # Session inactive until that occurs; anything unexpected leads to immediate closure.
  # See, also, doc header for structured_msg.capnp AuthHeader.sessionToken.
  #
  # Note that, as of this writing, both sides are expected to have exact agreement on the details of the transports
  # used for messages along this channel namely:
  #   - Whether to use an MQ pipe (for blobs/messages only).
  #     - If so its type (MqType).
  #   - Whether native socket transmission is enabled (if not then only blobs/messages can be transmitted).
  # As a result, a certain combination of MQ pipes and/or a socket stream (Unix domain stream socket connection)
  # shall be configured.  This might even be a compile-time decision; indeed these are all *template params* to
  # ipc::transport::Channel.
  #
  # Given that fact, it might be surprising that these details are also encoded in this message; why specify
  # knobs the other side must already know and have agreed-to in some sense?  Answer: It's not strictly necessary,
  # but it is (1) cheap and (2) a nice verification mechanism at least.
  #
  # As noted above the following 2 knobs, as of this writing, are for verification purposes only.

  protocolNegotiationToServer @0 :ProtocolNegotiation;
  # Protocol capability information that client is advertising about itself to server.
  # Upon receiving the LogInReq, server -- before processing any further fields -- ensures that it supports
  # min(<this>, <its own preferred (highest) version>) (for each layer in ProtocolNegotiation).  If so, continue.
  # If not, close session immediately (protocol negotiation failed: they don't speak a common tongue).
  # Since we have a SYN/SYN_ACK exchange built-in to the protocol from the get-go, we piggy-back (piggy-front?)
  # this exchange of info onto that exchange.  Hence when LogInRsp responds with similar info, the client
  # performs a similar check (and, similarly, closes session if protocol negotiation fails on its side).
  #
  # That said: As of this writing there is only version 1, so everything else is simple.  If/when there is a version 2+,
  # *and* *if* client-side software wants to attempt to support backwards compatibility (meaning speaking more than 1
  # version, the highest/preferred one it knows), then the future additions -- if any! -- to LogInReq must
  # be quite carefully managed: at the time of creating LogInReq, the client does not yet know of the server's
  # capabilities, and therefore does not know which protocol-version it should speak.  Hence it needs to send stuff
  # supported by all server versions that could possibly speak to it (in LogInReq specifically).  After LogInRsp
  # is received and verified, the client can zero in on the exact version to speak (or explode the channel/session).

  mqTypeOrNone @1 :MqType;
  # Verified knob: Is an MQ pipe enabled (if not then `none`); if so its type (`posix`, `bipc`, whatever).

  nativeHandleTransmissionEnabled @2 :Bool;
  # Verified knob: Is it, in addition to a message, possible to pair it with a native socket?

  claimedOwnProcessCredentials @3 :ProcessCredentials;
  # Client's own PID/UID/GID reported by application level.  At least compared to OS-reported values from connection if
  # possible; discrepancy means disconnect (log-in fail).

  ownApp @4 :ClientApp;
  # Client's application information.  At least compared to centrally configured values that list all possible
  # client applications and their details; discrepancy means disconnect (log-in fail) -- as does attempt of client
  # app X to connect to server app Y, when no such conversation is centrally configured as valid.

  shmTypeOrNone @5 :ShmType;
  # ipc::session-created `Session`s are optionally SHM-enabled (this is specified at compile time: one can use
  # either just a, e.g., Client_session; or a shm::classic::Client_session which is identical but adds
  # construction/GC/transmission APIs for various SHM scopes); this is the client's declared intention to
  # be SHM-enabled via the given provider; or not at all.  Similarly to "verified knobs" in OpenChannel* below,
  # this is a verification mechanism only; as both sides must agree to the same thing at compile time.
  # If not then the response to LogInReq would be immediate closure (similarly to, say, a
  # claimedOwnProcessCredentials verification failing).  Further protocol:
  #   - none: No additional protocol.  SHM disabled.
  #   - classic: No additional protocol: Before returning LogInRsp, server creates SHM pools (1 per the following scopes
  #     as of this writing: per-session, per-app) whose names are agreed-upon by convention; which
  #     the client also knows hence opens the pools at those names.  E.g., all scopes' names are based on a certain
  #     prefix which depends on the srv-app-name (both sides know it), srv-namespace (both sides know it before
  #     session starts) and cli-app-name (client knows it and sends it to server via
  #     ownApp above); and some of those scopes depends on cli-namespace (server determines it and sends it
  #     to client in LogInRsp).
  #   - jemalloc: Client shall expect 1 JemallocShmSetup message.  See below.

  metadata @6 :Metadata(MetadataPayload);
  # Metadata similar to OpenChannel*Req.metadata.  There is an opposite-facing LogInRsp.metadata also.
  # These could be used to describe the numInitChannelsByCliReq channels we want opened on our behalf;
  # or really any other at-session-open information.

  numInitChannelsByCliReq @7 :Size;
  # Client is requesting this many channels (possibly 0) to be opened on its behalf and returned
  # as the init_channels_by_cli_req set (to the on-session-opened-OK handler, on each side).
  # LogInRsp.numInitChannelsBySrvReq is similar from the server side.
  #
  # This mechanism is provided, in case the applications want a channel set ready right away with the opened
  # session.  OpenChannel* requires an asynchronous setup, at least on the passive-open side, and often one would
  # rather just skip it and have everything ready to go from the start; so this is the alternative or complement
  # to it.
} # struct LogInReq

struct LogInRsp(MetadataPayload)
{
  # Response to LogInReq.  If received then log-in successful.  If channel closed instead the unsuccessful.
  # TODO: Consider adding failed LogInRsp variant with reason code a-la OpenChannelResult.

  protocolNegotiationToClient @0 :ProtocolNegotiation;
  # Identical stuff to LogInReq.protocolNegotiationToServer -- see that guy.
  # So assuming server is happy and sends LogInRsp, client performs identical check on these data.
  # If it's not happy (even though server was, hence why it even responses with LogInRsp), *it* closes session.

  clientNamespace @1 :ClientNamespace;

  metadata @2 :Metadata(MetadataPayload);
  # See LogInRsp.  Symmetrical thing but srv->cli.

  numInitChannelsBySrvReq @3 :Size;
  # See LogInRsp.  Symmetrical to numInitChannelsByCliReq but srv->cli.
  # Returned on each side as init_channels_by_srv_req.
}

struct JemallocShmSetup
{
  # See LogInReq.shmTypeOrNone = jemalloc.  This is the server->client setup message the client shall expect
  # before considering the setup complete.  Currently the message itself shall contain nothing; but it
  # must be accompanied by a pre-connected peer native socket (1 of 2), establishing a simple
  # Unix-domain-socket-blobs-only channel that is required for internal use by SHM-jemalloc provider internals.
  #
  # If there is no socket, it indicates server failed to create the socket-pair (extremely surprising especially since
  # the session-master-channel itself needed one and apparently was created fine -- but stuff happens).
}

struct OpenChannelToClientReq(MetadataPayload)
{
  # Server->client notification of opening a bidirectional channel.  (See also OpenChannelToServerReq for alternative
  # client->server flow.)  OpenChannelToClientRsp response is required to complete the channel opening.
  # Note, however, that the response message to this is merely an ACK: All shared transport resources are already
  # opened/connected/ready just before this message is sent.  I.e., the named MQs (if any) are created;
  # the socket stream connection (if any) is pre-connected via socket-pair OS call.  (The same is not true
  # of the client->server flow.)
  #
  # If and only if LogInReq.nativeHandleTransmissionEnabled = true, then the present message
  # must be accompanied by a pre-connected peer native socket (1 of 2).

  clientToServerMqAbsNameOrEmpty @0 :SharedName;
  # If and only if LogInReq.mqTypeOrNone =/= `none`: Absolute Shared_name of the MQ client->server unidirectional MQ.
  serverToClientMqAbsNameOrEmpty @1 :SharedName;
  # Ditto but for server->client unidirectional MQ.

  metadata @2 :Metadata(MetadataPayload);
  # A generically-typed value such that (1) the user actively-opening the channel shall supply it, and (2)
  # the user accepting the passive-open shall receive it.  MetadataPayload must be a type that is (1)
  # constant across *all* `OpenChannelToClientReq`s in a particular session and (2) is the same type
  # on the receiving side.  (Otherwise undefined runtime behavior results: at best a capnp exception at some point.)
  #
  # What's the use case?  Answer: The original use case: A channel, on successful open, is not
  # (necessarily) structured: channels can transmit blobs and possibly native sockets.  Technically it can remain
  # unstructured: it's certainly conceivable that, for perf reasons for instance, that's what the app writer
  # wants.  Arguably more mainstream, though, is that it will be immediately (and permanently) interpreted by
  # both sides as structured from that point on (i.e., it will transmit structured messages and optionally
  # native sockets -- just like *this* session master channel).  In C++: a C=Channel<> is opened; then wrapped
  # in a struc::Channel<C, M> from that point on, on both sides.  But what is M, namely the Message_body
  # template param?  That's the schema used for that channel's conversation; it describes every message's
  # possible contents (just like *this* session master channel's SessionMasterChannelMessageBody, a fine
  # example if you want one right now; in fact this OpenChannelToClientReq is 1 possible msg in that schema).
  #
  # It is very much conceivable that the app is simple enough to where every user structured channel in a given
  # session (maybe all sessions) will use one schema (one value for Message_body); but it's also conceivable
  # that parts A and B of the application are concerned with disparate features.  It would be uncouth to force
  # them both to share one giant union with all possible messages, even across totally unrelated features A and B.
  # No problem: each struc::Channel<C, M> can specify whatever M it wants, as long as both sides specify the
  # same M type.  However:
  #
  # Problem: if the application can use schema M_a and schema M_b,
  # and a passive channel-open request arrives and is accepted, how does
  # the passive-open-accepting user code know *which* schema pertains to this particular channel?  There's a
  # chicken/egg problem: one must know M to construct struc::Channel<C, M> around the C, but to communicate
  # about what M is (or anything else), in a structured way at least, it needs the struc::Channel already.
  # So the solution here is that this bit of metadata supplies whatever arbitrary info one needs to denote this;
  # it can be as simple as a flag or enumeration (but it has to be inside a struct) or much more complex.
  # (Informally we suggest keeping it simple: The structured conversing should occur within the channel, not within
  # this metadata which is meant only to enable that structured conversing to begin.)
  #
  # The key is that MetadataPayload *is* always the same type; otherwise the same chicken/egg problem
  # would've been in effect.
  #
  # If no such metadata is required, use MetadataPayload=void and then simply ignore it on both
  # sides of the channel-open.  (The C++ ipc::session APIs include signatures that omit this thing.)
} # struct OpenChannelToClientReq

struct OpenChannelToClientRsp
{
  # Response to OpenChannelToClientReq.

  openChannelResult @0 :OpenChannelResult;
  # Only `accepted` indicates success.
}

struct OpenChannelToServerReq(MetadataPayload)
{
  # Client->server notification of opening a bidirectional channel.  (See also OpenChannelToClientReq for the
  # alternative server->client flow.)  OpenChannelToServerRsp reponse is required to complete the channel opening.
  #
  # Keeping comments light, in that most comments for OpenChannelToClientReq apply here.
  # However, this is more of a 2-hop exchange, because in our design the server side is still the one expected to create
  # the necessary shared resources (MQs if any, socket stream if any) and then inform the client of the results.
  # So the information in OpenChannelToClientReq is instead split among OpenChannelToServerReq and
  # OpenChannelToServerRsp.

  metadata @0 :Metadata(MetadataPayload);
}

struct OpenChannelToServerRsp
{
  # Response to OpenChannelToServerReq.
  # If and only if LogInReq.nativeHandleTransmissionEnabled = true, then the present message
  # must be accompanied by a pre-connected peer native socket (1 of 2).
  # (That's assuming openChannelResult=accepted.)

  openChannelResult @0 :OpenChannelResult;
  # Only `accepted` indicates success.

  clientToServerMqAbsNameOrEmpty @1 :SharedName;
  # Ignore if not `accepted` above.
  serverToClientMqAbsNameOrEmpty @2 :SharedName;
  # Ignore if not `accepted` above.
}

struct GracefulSessionEnd
{
  # Side A sending this to side B means, "the Session object destructor in A has begun to execute, which means any
  # user resources that must be freed before the session can fully close have indeed been freed."
  # Session_base::Graceful_finisher doc header contains the explanation.  Spoiler alert: As of this writing this
  # is needed, and used, only by SHM-jemalloc (shmTypeOrNone = jemalloc) sessions; and the "user resources that
  # must be freed before the session can fully close" are handles (cross-process shared_ptr<>s) from
  # session.session_shm()->construct<>()ed (where `session` is side B `Session` object) that have been
  # .borrow_object()ed by side A.  So side A sending this to side B is saying, "they're calling out dtor, so
  # they have to have freed any stuff they've borrowed from you, so you can now destroy your Session object including
  # this SHM arena."  But do see aforementioned doc header which explores it more methodically.

  # No fields necessary for now.
}

struct ProtocolNegotiation
{
  # Conceptually (and as of this writing in every detail) identical to the one in structured_msg.capnp.
  # Since they are separate protocol layers, it seemed prudent not to reuse the one in structured_msg.capnp,
  # in case we need to expand/modify/? one or the other.

  maxProtoVer @0 :ProtoVer;
  maxProtoVerAux @1 :ProtoVer;
}
